#include <cstdio>
#include <cstring>
#include <iostream>
#include <libgen.h>
#include "xor_file.h"
#include "md5.h"
#include "base64.h"

typedef long long WORD_UNIT;
const int WORD_LEN = sizeof(WORD_UNIT);
const int OFFSET_LIMIT = 16 - WORD_LEN;
const int BUFF_LEN = 1 * 1024 * 1024;

int encrypt_file_content(const char *const path, const string &passwd)
{
    // get md5 bytes of password
    unsigned char hash_bytes[16];
    md5bin(passwd.c_str(), passwd.length(), hash_bytes);
    // the circle keys for encryption
    const WORD_UNIT *bytes_to_word_ptr = reinterpret_cast<const WORD_UNIT *>(hash_bytes);
    // open file using read and write mode
    int bytes_read = -1;
    int hash_bytes_offset = 0;
    bool ret = true;
    FILE *file = fopen(path, "r+");
    if (file == NULL)
    {
        printf("cannot open %s \n", path);
        return -1;
    }
    char *buff = (char *)malloc(BUFF_LEN);

    while (bytes_read != 0)
    {
        // read the content of file to buff
        bytes_read = fread(buff, 1, BUFF_LEN, file);
        if (bytes_read > 0)
        {
            // do encryption in buff
            WORD_UNIT *content = reinterpret_cast<WORD_UNIT *>(buff);
            for (int i = 0; i < bytes_read; i += WORD_LEN)
            {
                hash_bytes_offset = (hash_bytes_offset >= OFFSET_LIMIT ? 0 : hash_bytes_offset + 1);
                bytes_to_word_ptr = reinterpret_cast<const WORD_UNIT *>(hash_bytes + hash_bytes_offset);
                *content = *content ^ *bytes_to_word_ptr;
                content++;
            }
            // write encrypted data back to file
            ret &= fseek(file, -bytes_read, SEEK_CUR) == 0;
            ret &= fwrite(buff, 1, bytes_read, file) == bytes_read;
            fflush(file);
            if (!ret)
            {
                printf("fseek or fwrite error\n");
                return -2;
            }
        }
    }
    free(buff);
    fclose(file);
    return 0;
}

string encrypt_file_content(const string &byte_array, const string &passwd)
{
    // get md5 bytes of password
    unsigned char hash_bytes[16];
    md5bin(passwd.c_str(), passwd.length(), hash_bytes);
    // the circle keys for encryption
    const WORD_UNIT *bytes_to_word_ptr = reinterpret_cast<const WORD_UNIT *>(hash_bytes);
    // put data of the string to buff
    int hash_bytes_offset = 0;
    int padding = WORD_LEN - (byte_array.length() % WORD_LEN);
    char *buff = (char *)malloc(byte_array.length() + padding);
    strncpy(buff, byte_array.c_str(), byte_array.length());
    // do encryption
    WORD_UNIT *content = reinterpret_cast<WORD_UNIT *>(buff);
    for (int i = 0; i < byte_array.length(); i += WORD_LEN)
    {
        hash_bytes_offset = (hash_bytes_offset >= OFFSET_LIMIT ? 0 : hash_bytes_offset + 1);
        bytes_to_word_ptr = reinterpret_cast<const WORD_UNIT *>(hash_bytes + hash_bytes_offset);
        *content = *content ^ *bytes_to_word_ptr;
        content++;
    }
    string ret(buff, byte_array.length());
    free(buff);
    return ret;
}

int decrypt_file_content(const char *const path, const string &passwd)
{
    return encrypt_file_content(path, passwd);
}

string decrypt_file_content(const string &byte_array, const string &passwd)
{
    return encrypt_file_content(byte_array, passwd);
}

string encrypt_filename(const string &name, const string &passwd)
{
    string ret = base64_encode(encrypt_file_content(name, passwd), false);
    for (auto &item : ret)
    {
        if (item == '=')
            item = '-';
        if (item == '/')
            item = '_';
    }
    return ret;
}

string decrypt_filename(string name, const string &passwd)
{
    for (auto &item : name)
    {
        if (item == '-')
            item = '=';
        if (item == '_')
            item = '/';
    }
    return decrypt_file_content(base64_decode(name), passwd);
}

void rename_file(string abs_file_path, string new_filename)
{
    char *dirc, *dname;
    dirc = strdup(abs_file_path.c_str());
    dname = dirname(dirc);
    new_filename = dname + ('/' + new_filename);
    free(dirc);
    dname = nullptr;
    rename(abs_file_path.c_str(), new_filename.c_str());
}